package simbot.example.utils.client;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpMethod;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

/**
 * 新HttpClient工具类  支持Builder
 *
 * @author minmin
 * @since 2021 /07/21 0:51
 */
@Slf4j
public class HttpClientUtils {

    private static final PoolingHttpClientConnectionManager CM;
    private static final IdleConnectionThread IDLE_CONNECTION_THREAD;

    static {
        CM = new PoolingHttpClientConnectionManager();
        CM.setMaxTotal(100);
        CM.setDefaultMaxPerRoute(20);
        // 启动该线程，负责清理无效连接
        IDLE_CONNECTION_THREAD = new IdleConnectionThread(CM);
        IDLE_CONNECTION_THREAD.start();
    }

    private final CloseableHttpClient client;
    private final HttpRequestBase httpRequestBase;

    private HttpClientUtils(Builder builder) {
        String url = builder.url;
        url = builder.queryParams == null ? url : url + builder.queryParams;
        log.debug("http builder url: {}", url);
        httpRequestBase = getHttpRequestBase(builder.httpMethod);
        log.debug("http builder method: {}", builder.httpMethod);
        if (builder.headMap == null) {
            httpRequestBase.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
        } else {
            builder.headMap.forEach(httpRequestBase::setHeader);
        }
        log.debug("http builder headers: {}", Arrays.toString(httpRequestBase.getAllHeaders()));
        httpRequestBase.setURI(URI.create(url));
        if (builder.body != null) {
            if (httpRequestBase instanceof HttpEntityEnclosingRequest) {
                HttpEntityEnclosingRequest httpEntityEnclosingRequest = (HttpEntityEnclosingRequest) this.httpRequestBase;
                try {
                    StringEntity stringEntity = new StringEntity(builder.body);
                    httpEntityEnclosingRequest.setEntity(stringEntity);
                    log.debug("http builder body: {}", builder.body);
                } catch (UnsupportedEncodingException e) {
                    // 如果不支持HttpClient默认的Http字符集则报错
                    e.printStackTrace();
                }
            }
        }
        client = getCloseableHttpClient(builder.httpHost);
    }

    /**
     * Close.
     */
    public static void close() {
        IDLE_CONNECTION_THREAD.interrupt();
        CM.close();
    }

    private CloseableHttpClient getCloseableHttpClient(HttpHost proxy) {
        return proxy == null
                ? HttpClients.custom()
                .setConnectionManager(CM)
                .build()
                : HttpClients.custom()
                .setConnectionManager(CM)
                .setProxy(proxy)
                .build();
    }

    /**
     * Send request response result.
     *
     * @return the response result
     * @throws IOException the io exception
     */
    public ResponseResult sendRequest() throws IOException {
        try (CloseableHttpResponse closeableHttpResponse = client.execute(httpRequestBase)) {
            return new ResponseResult(closeableHttpResponse);
        }
    }

    private HttpRequestBase getHttpRequestBase(HttpMethod httpMethod) {
        switch (httpMethod) {
            case GET:
                return new HttpGet();
            case POST:
                return new HttpPost();
            case PUT:
                return new HttpPut();
            case HEAD:
                return new HttpHead();
            case PATCH:
                return new HttpPatch();
            case TRACE:
                return new HttpTrace();
            case DELETE:
                return new HttpDelete();
            case OPTIONS:
                return new HttpOptions();
            default:
                throw new RuntimeException("no case");
        }
    }

    /**
     * The type Idle connection thread.
     */
    static class IdleConnectionThread extends Thread {

        private final HttpClientConnectionManager connMgr;

        /**
         * Instantiates a new Idle connection thread.
         *
         * @param connMgr the conn mgr
         */
        public IdleConnectionThread(HttpClientConnectionManager connMgr) {
            this.connMgr = connMgr;
        }

        @Override
        public void run() {
            try {
                while (!isInterrupted()) {
                    synchronized (this) {
                        wait(5000);
                        // 关闭失效的连接
                        connMgr.closeExpiredConnections();
                    }
                }
            } catch (InterruptedException ex) {
                // 结束
            }
        }
    }

    /**
     * The type Response result.
     */
    @Getter
    public static class ResponseResult {
        private final Map<String, String> headers = new HashMap<>();
        private final String entity;
        private final Long contentLength;
        private final Integer statusCode;
        private final ProtocolVersion protocolVersion;
        private final Locale locale;

        /**
         * Instantiates a new Response result.
         *
         * @param closeableHttpResponse the closeable http response
         * @throws IOException the io exception
         */
        private ResponseResult(CloseableHttpResponse closeableHttpResponse) throws IOException {
            for (Header header : closeableHttpResponse.getAllHeaders()) {
                headers.put(header.getName(), header.getValue());
            }
            Header encoding = closeableHttpResponse.getEntity().getContentEncoding();
            if (encoding != null) {
                Charset defaultCharset = Charset.forName(encoding.getName());
                entity = EntityUtils.toString(closeableHttpResponse.getEntity(), defaultCharset);
                headers.put(encoding.getName(), encoding.getValue());
            } else {
                entity = EntityUtils.toString(closeableHttpResponse.getEntity(), StandardCharsets.UTF_8);
            }
            contentLength = closeableHttpResponse.getEntity().getContentLength();
            Header contentType = closeableHttpResponse.getEntity().getContentType();
            headers.put(contentType.getName(), contentType.getValue());
            statusCode = closeableHttpResponse.getStatusLine().getStatusCode();
            protocolVersion = closeableHttpResponse.getProtocolVersion();
            locale = closeableHttpResponse.getLocale();
        }
    }

    /**
     * The type Builder.
     */
    @AllArgsConstructor
    public static class Builder {
        private String url;
        private StringBuffer queryParams;
        private Map<String, String> headMap;
        private String body;
        private HttpMethod httpMethod;
        private HttpHost httpHost;

        /**
         * Instantiates a new Builder.
         *
         * @param url        the url
         * @param httpMethod the http method
         */
        public Builder(String url, HttpMethod httpMethod) {
            this.url = Objects.requireNonNull(url);
            this.httpMethod = Objects.requireNonNull(httpMethod);
        }

        /**
         * Url builder.
         *
         * @param url the url
         * @return the builder
         */
        public Builder url(String url) {
            this.url = Objects.requireNonNull(url);
            return this;
        }

        /**
         * Query params builder.
         *
         * @param paramMap the param map
         * @return the builder
         */
        public Builder queryParams(Map<String, String> paramMap) {
            StringBuffer result = new StringBuffer("?");
            paramMap.forEach((k, v) -> result.append(k).append("&").append(v));
            return queryParams(result.toString());
        }

        /**
         * Query params builder.
         *
         * @param name  the name
         * @param value the value
         * @return the builder
         */
        public Builder queryParams(String name, String value) {
            return queryParams(name + "=" + value);
        }

        /**
         * Query params builder.
         *
         * @param queryParams the query params
         * @return the builder
         */
        public Builder queryParams(String queryParams) {
            if (this.queryParams == null) {
                this.queryParams = new StringBuffer("?");
            }
            this.queryParams.append(queryParams).append("&");
            return this;
        }

        /**
         * Head map builder.
         *
         * @param headMap the head map
         * @return the builder
         */
        public Builder headMap(Map<String, String> headMap) {
            this.headMap = headMap;
            return this;
        }

        /**
         * Body builder.
         *
         * @param body the body
         * @return the builder
         */
        public Builder body(String body) {
            this.body = body;
            return this;
        }

        /**
         * Request method builder.
         *
         * @param requestMethod the request method
         * @return the builder
         */
        public Builder requestMethod(String requestMethod) {
            return requestMethod(HttpMethod.resolve(requestMethod));
        }

        /**
         * Request method builder.
         *
         * @param httpMethod the http method
         * @return the builder
         */
        public Builder requestMethod(HttpMethod httpMethod) {
            this.httpMethod = Objects.requireNonNull(httpMethod);
            return this;
        }

        /**
         * Http host builder.
         *
         * @param host the host
         * @param port the port
         * @return the builder
         */
        public Builder proxy(String host, int port) {
            this.httpHost = new HttpHost(host, port);
            return this;
        }

        /**
         * Http host builder.
         *
         * @param host the host
         * @param port the port
         * @return the builder
         */
        public Builder proxy(String host, String port) {
            this.httpHost = new HttpHost(host, Integer.parseInt(port));
            return this;
        }

        /**
         * Build http client utils.
         *
         * @return the http client utils
         */
        public HttpClientUtils build() {
            return new HttpClientUtils(this);
        }
    }
}
